再看 JavaScript 继承
作者:Hyuain
https://juejin.im/post/5e43bb2df265da571670fd8b
学完了整个 JavaScript 基础篇章之后,发现自己对继承的理解有了好几次了翻天覆地的变化(扶额),于是就写了这样一篇文章阐述目前自己对继承的看法。其实是初到掘金,复制了一篇之前自己写的文章(笑)。
本文将着重讨论基于原型的继承,也会简单写一下如何用 class 继承。
Key Points
在 JavaScript 中,函数
Function
也是一种 对象Object
关于函数
所有函数都自带
prototype
prototype
中自带constructor
constructor
里面的东西就是函数的内容构造函数首字母大写(约定俗成)
对象.__proto__===其构造函数.prototype
〇、简单解释
首先,关于函数也是一种对象这个说法,我们在后面(也许是别的文章中)会有相关的说明,这里先记住这个结论即可;
其次是关于函数的几个描述,我们可以做几个实验来验证一下:
let arr = [1, 2, 3]
let obj = { name: 'Harvey', age: '22'}
let fn = function(){ console.log('hi') }
复制代码
打印出 arr
obj
和 fn
之后就可以看到,比起别的对象,函数确实是比较特别的,他天生就带有一个 prototype
属性,而且 prototype
中的 constructor
就是这个函数本身。
arr.prototype === undefined // true
obj.prototype === undefined // true
fn.prototype.constructor === fn // true
复制代码
我们也可以再验证一下最后一句话:
function Person(){}
let me = new Person
me.__proto__ === Person.prototype // true
复制代码
做了这几个小实验之后,我们进入正题来讨论。
一、原型链
原型链的精髓其实就是刚才已经提到过的一句话:
对象.
__proto__
=== 其构造函数.prototype
当然这样说比较抽象,我们可以展开对普通对象、数组(代表了比较特殊的对象,比如日期等),以及函数来分别进行讨论。
普通对象的原型链
普通对象的原型是
Object
这句话要从以下几点来理解:
创建一个对象可以按这种方式写:
letobj=newObject({name:'Harvey',age:'22'})
Object
实际上是一个构造函数,他构造了obj
obj.__proto__===Object.prototype
因此我们可以简单表示一下这个普通对象的原型链:
obj->Object.prototype
数组的原型链
数组的原型是
Array
这句话要从以下几点来理解:
创建一个数组可以按这种方式写:
letarr=newArray(1,2,3)
Array
实际上是一个构造函数,他构造了arr
arr.__proto__===Array.prototype
事实上,我们还会发现:
arr.__proto__.__proto__ === Object.prototype // true
Array.prototype.__proto__ === Object.prototype // true
复制代码
也就是说,一个数组的原型链要稍微复杂一些:
arr->Array.prototype->Object.prototype
函数的原型
函数的原型是
Function
这句话要从以下几点来理解:
创建一个数组可以按这种方式写:
letfn=newFunction((),{console.log('hi')})
Function 实际上是一个构造函数,他构造了 fn
fn.__proto__===Function.prototype
也就是说,一个函数的原型链也要稍微复杂一些:
fn->Function.prototype->Object.prototype
修改原型链
通过直接修改 __proto__
就可以达到修改原型链的目的
let obj1 = { a: 1 }
let obj2 = { b: 2 }
let obj3 = { c: 3 }
obj2.__proto__ = obj1
obj3.__proto__ = obj2
复制代码
这样,他们的原型链就变成了:obj3->obj2->obj1->Object.prototype
但是这种方法是不推荐的,我们更推荐使用 Object.create()
方法,他的使用方法如下:
let obj1 = { a: 1 }
let obj2 = Object.create(obj1)
obj2.b = 2
let obj3 = Object.create(obj2)
obj3.c = 3
复制代码
这样与上面直接修改 __proto__
效果基本是一样的
二、继承
这里我们要明确一点,平时大家所说的继承(或者说类的继承),其实更多的是一种 狭义的继承。他指的 不是 我们按照上面的方式 单纯对原型链进行的修改,而是 一种在 构造函数之间 的,在 prototype
之间的继承。
比如我们说 Array
继承了 Object
:
// Array 继承了 Object:
Array.prototype.__proto === Object.prototype
复制代码
而不说 arr
继承了 Array
,哪怕出现了原型链:
// 我们不说 arr 继承了 Array
arr.__proto__ === Array.prototype
复制代码
也不说 obj2
继承了 obj1
,哪怕出现了 __proto__
:
// 我们也不说 obj2 继承了 obj1
obj2.__proto === obj1
复制代码
那么问题来了,这种在 构造函数之间的继承 应该怎么写呢,怎样才能得到像 Array
和 Object
的这种关系呢?
第一步:使用 call
来调用父类构造函数
// 定义父类
function Person(姓名) {
this.姓名 = 姓名
}
Person.prototype.自我介绍 = function() {
console.log(`你好,我是 ${this.姓名}`)
}
复制代码
// 尝试定义一个子类,来继承 Person
function Student(姓名, 学号){
Person.call(this, 姓名) // 调用父类构造函数
this.学号 = 学号
}
复制代码
这一步是为了让 new子类
创建出来的对象拥有与 new父类
一样的属性。
在这个例子中,就是为了让 newStudtent
创建出来的对象,拥有 Person
中的 姓名
属性。
好,我们现在来尝试创建一个 Student
对象 小明
。
let 小明 = new Student('小明', 123456)
复制代码
小明.姓名 // '小明'
小明.学号 // 123456
小明.自我介绍() // Uncaught TypeError: 小明.自我介绍 is not a function
复制代码
小明
如何拿到 学号
?
在 newStudent('小明',123456)
的时候,系统会去调用 Student
函数,并且把 小明
这个对象作为 this
传进去;
相当于在 Student
函数中执行了 小明.学号=123456
。
小明
如何拿到 姓名
?
同样,系统调用 Student
函数,看到了 Person.call(this,姓名)
(相当于 Person.call(小明,'小明')
),意思是让 Person
中的 this
为 小明
,并且传一个参数 '小明'
给 Person
;
然后将会调用 Person
函数,在 Person
中执行 this.姓名=姓名
(相当于 小明.姓名='小明'
)。
为什么 小明
不能使用 自我介绍
?
因为可以看到, 小明
这个对象中没有 自我介绍
属性,他的 __proto__
(也就是 Student.prototype
) 中也没有,因此他找不到 自我介绍
。
第二步:建立原型链
那么我们怎样才能让 小明
能够进行 自我介绍
呢?想到了几种写法,我们来一一分析一下:
1、直接将 自我介绍
放在 小明
这个对象实例上
小明.自我介绍 = function(...){...}
复制代码
但是既然每个人都需要 自我介绍
,那么我们单独修改 小明
这样一个对象就没有意义。
2、将 自我介绍
放在 小明
这一类对象实例上
Student = function() {
this.自我介绍 = Person.prototype.自我介绍
}
复制代码
这样最终其实是可以实现相同的效果的。不过这样就需要每个函数都单独写一下,有时候也不太方便。
3、将 自我介绍
放在 小明
这一类对象实例的 __proto__
上
众所周知, 小明
也可以访问到 小明.__proto__
上的函数,所以这好像也是可行的。
既然 小明.__proto__===Student.prototype
,那要不我们这样写:
Student.prototype = Person.prototype
复制代码
这样 小明
就可以使用在 Person.prototype
上的 自我介绍
了。
但是!如果这个时候 Student
想要给自己的 prototype
加一个新方法,怎么办?我们知道因为只是复制了地址,如果修改了 Student.prototype
, Person.prototype
也将被修改,这显然是我们不愿意看到的。难道要用深拷贝?orz
4、将 自我介绍
放在 小明
这一类对象实例的 __proto__
的 __proto__
上
众所周知, 小明
也可以访问到 小明.__proto__.__proto__
上的函数,所以这也是可行的。
既然 小明.__proto__===Student.prototype
,那么也就是说我们要实现:
Student.prototype.__proto__ = Person.prototype
复制代码
这就是我们的终极解决方案啦,是不是看起来有点眼熟?
Array.prototype.__proto__ === Object.prototype // true
复制代码
对了!这就是原型链!这就是我们所说的继承!是不是有点感觉了?
当然,刚才也说了,我们最好用下面这种写法:
Student.prototype = Object.create(Person.prototype)
复制代码
回顾一下到现在我们做了什么?
首先我们通过
call父级构造函数
,来实现属性的继承,我们的小明
有了姓名
然后我们通过建立原型链,来实现方法的继承,我们的
小明
可以自我介绍
了
看似已经结束,但是实际上还有一个隐藏的 Bug,我们接下来来解决这个 Bug。
第三步:解决 constructor 的问题
细心的你会发现(我们在最开始也说过了),我们的对象实例和构造函数中是有一个 constructor
属性的,比如:
const arr = [1, 2]
arr.__proto__.constructor === Array // true
Array.prototype.constructor === Array // true
复制代码
但是, Student.prototype
中的 constructor
被刚才的那一番操作给搞没了,我们需要把它弄回来:
Student.prototype.constructor = Student
复制代码
这样就完成了一波类的继承。
三、总结
看一波完整的代码:
// 定义父类
function Person(姓名) {
this.姓名 = 姓名
}
// 定义父类的方法
Person.prototype.自我介绍 = function() {
console.log(`你好,我是 ${this.姓名}`)
}
// 定义子类
function Student(姓名, 学号){
Person.call(this, 姓名) // 调用父级构造函数,继承父类的属性
this.学号 = 学号
}
// 建立原型链,继承父类的方法
Student.prototype = Object.create(Person.prototype)
// 解决 constructor 问题
Student.prototype.constructor = Student
// 定义子类的新方法
Student.prototype.报数 = function() {
console.log(`我的学号是 ${this.学号}`)
}
let 小红 = new Student('小红', 345678)
小红.自我介绍() // 你好,我是 小红
小红.报数() // 我的学号是 345678
复制代码
四、用 class 继承
既然他本身就是语法糖,我个人认为没必要搞那么细,其实本质跟上面的使用原型链的继承是一样的,搞清楚是怎么写的就好啦:
// 定义父类
class Person {
constructor(姓名) { // 定义属性
this.姓名 = 姓名
}
自我介绍() { // 定义方法
console.log(`你好,我是 ${this.姓名}`)
}
}
// 定义子类
class Student extends Person {
constructor(姓名, 学号) {
super(姓名) // 这里的 姓名 两个字要与父类中的一样,继承属性和方法
this.学号 = 学号 // 定义新属性
}
报数() { // 定义新方法
console.log(`我的学号是 ${this.学号}`)
}
}
let 小红 = new Student('小红', 345678)
小红.自我介绍() // 你好,我是 小红
小红.报数() // 我的学号是 345678
复制代码
看完两件事
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
关注公众号「前端开发博客」,持续为你推送精选好文
加入微信群👆,每日分享全网好文章!